#!/usr/bin/env python3
#
# Copyright (c) 2025      Jeffrey M. Squyres.  All rights reserved.
# $COPYRIGHT$
#
# Additional copyrights may follow
#
# $HEADER$
#

import os
import sys
import argparse

def find_help_files(root, verbose=False):
    # Search for help-*.txt files across the source tree, skipping
    # some directories (e.g., 3rd-party)
    help_files = []
    skip_dirs = ['.git', '3rd-party']
    for root_dir, dirs, files in os.walk(root):
        for sd in skip_dirs:
            if sd in dirs:
                dirs.remove(sd)

        for file in files:
            if file.startswith("help-") and file.endswith(".txt"):
                full_path = os.path.join(root_dir, file)
                help_files.append(full_path)
                if verbose:
                    print(f"Found: {full_path}")
    return help_files

def parse_ini_files(file_paths, verbose=False):
    # Parse INI-style files, returning a dictionary with filenames as
    # keys.  Don't use the Python configparse module in order to
    # reduce dependencies (i.e., so that we don't have to pip install
    # anything to run this script).
    data = {}
    for file_path in file_paths:
        sections = {}
        current_section = None
        with open(file_path) as file:
            for line in file:
                line = line.rstrip()
                if line.startswith('#'):
                    continue
                if line.startswith('[') and line.endswith(']'):
                    current_section = line[1:-1]
                    sections[current_section] = list()
                elif current_section is not None:
                    sections[current_section].append(line)

        data[os.path.basename(file_path)] = sections

        if verbose:
            print(f"Parsed: {file_path} ({len(sections)} sections found)")

    return data

def generate_c_code(parsed_data):
    # Generate C code with an array of filenames and their
    # corresponding INI sections.
    c_code = []
    c_code.append(f"""// THIS FILE IS GENERATED AUTOMATICALLY! EDITS WILL BE LOST!
// This file generated by {sys.argv[0]}

""")
    # Rather than escaping the C code {} in f strings, make this a
    # separate (non-f-string) addition to c_code.
    c_code.append("""#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "opal/util/argv.h"
#include "opal/util/show_help.h"

typedef struct {
    const char *section;
    const char **content;
    char *joined;
} ini_entry;

typedef struct {
    const char *filename;
    ini_entry *entries;
} file_entry;

static file_entry help_files[] = {""")

    ini_arrays = []
    file_entries = []

    sp4 = f'{" " * 4}'
    sp8 = f'{" " * 8}'
    sp12 = f'{" " * 12}'
    sp16 = f'{" " * 16}'
    sp20 = f'{" " * 20}'

    for filename, sections in parsed_data.items():
        c_code.append(f'{sp4}{{')
        c_code.append(f'{sp8}.filename = "{filename}",')
        c_code.append(f'{sp8}.entries = (ini_entry[]) {{')

        for section_name, lines in sections.items():
            c_code.append(f'{sp12}{{')
            c_code.append(f'{sp16}.section = "{section_name}",')
            c_code.append(f'{sp16}.content = (const char*[]) {{')
            for line in lines:
                c_string = line.replace('"','\\"').replace("\n", '\\n"\n"')
                c_code.append(f'{sp20}"{c_string}",')
            c_code.append(f'{sp20}NULL')
            c_code.append(f'{sp16}}},')
            c_code.append(f'{sp12}}}, // End of section {section_name}')

        c_code.append(f'{sp12}{{0}}')
        c_code.append(f'{sp8}}}, // End of file {filename}')
        c_code.append(f'{sp4}}},')

    c_code.append(f'{sp4}{{0}}')
    c_code.append(f'}};')

    c_code.append("""
const char *opal_show_help_get_content(const char *filename, const char* topic)
{
    const file_entry *fe;
    ini_entry *ie;

    for (int i = 0; help_files[i].filename != NULL; ++i) {
        fe = &(help_files[i]);
        if (strcmp(fe->filename, filename) == 0) {
            for (int j = 0; fe->entries[j].section != NULL; ++j) {
                ie = &(fe->entries[j]);
                if (strcmp(ie->section, topic) == 0) {
                    if (NULL == ie->joined) {
                        ie->joined = opal_argv_join((char**)ie->content, '\\n');
                    }
                    return ie->joined;
                }
            }
        }
    }

    return NULL;
}

void opal_show_help_content_free(void)
{
    for (int i = 0; help_files[i].filename != NULL; ++i) {
        for (int j = 0; help_files[i].entries[j].section != NULL; j++) {
            if (NULL != help_files[i].entries[j].joined) {
                free(help_files[i].entries[j].joined);
                help_files[i].entries[j].joined = NULL;
            }
        }
    }
}
""")

    return '\n'.join(c_code)

#-------------------------------

def main():
    parser = argparse.ArgumentParser(description="Generate C code from help text INI files.")
    parser.add_argument("--root",
                        required=True,
                        help="Root directory to search for help-*.txt files")
    parser.add_argument("--out",
                        required=True,
                        help="Output C file")
    parser.add_argument("--verbose",
                        action="store_true",
                        help="Enable verbose output")
    args = parser.parse_args()

    if args.verbose:
        print(f"Searching in: {args.root}")

    file_paths = find_help_files(args.root, args.verbose)
    parsed_data = parse_ini_files(file_paths, args.verbose)
    c_code = generate_c_code(parsed_data)

    if os.path.exists(args.out):
        with open(args.out) as f:
            existing_content = f.read()

            if existing_content == c_code:
                if args.verbose:
                    print(f"Help string content has not changed; not re-writing {args.out}")
                exit(0)

    with open(args.out, "w") as f:
        f.write(c_code)

    if args.verbose:
        print(f"Generated C code written to {args.out}")

if __name__ == "__main__":
    main()
